package com.klikmee.sms.core.base.communicator.mts;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.management.RuntimeErrorException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.MessageContext;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.log4j.Logger;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSPasswordCallback;
import org.apache.ws.security.handler.WSHandlerConstants;

import com.klikmee.sms.core.base.Core;
import com.klikmee.sms.core.base.communicator.Communicator;
import com.klikmee.sms.core.base.communicator.SendSmsIntercepter;
import com.klikmee.sms.core.base.dao.ProviderDAO;
import com.klikmee.sms.core.base.subscription.NoDistributionListFoundException;
import com.klikmee.sms.core.base.subscription.SubscriptionManager;
import com.klikmee.sms.core.base.util.Constants;
import com.klikmee.sms.core.base.util.DeliveryReports;
import com.klikmee.sms.core.model.BulkMessage;
import com.klikmee.sms.core.model.IncomingMessage;
import com.klikmee.sms.core.model.OutgoingMessage;
import com.klikmee.sms.core.model.Provider;
import com.klikmee.sms.core.ws.mts.client.receivemsg.ReceiveMessage;
import com.klikmee.sms.core.ws.mts.client.receivemsg.ReceiveMessageService;
import com.klikmee.sms.core.ws.mts.client.sendsms.ChargingInformation;
import com.klikmee.sms.core.ws.mts.client.sendsms.PolicyException_Exception;
import com.klikmee.sms.core.ws.mts.client.sendsms.SendSms;
import com.klikmee.sms.core.ws.mts.client.sendsms.SendSmsService;
import com.klikmee.sms.core.ws.mts.client.sendsms.ServiceException_Exception;
import com.klikmee.sms.core.ws.mts.client.sendsms.SimpleReference;
import com.klikmee.sms.core.ws.mts.listener.MTSCommunicator;
import com.klikmee.sms.core.ws.mts.listener.msgnotification.MessagePriority;
import com.klikmee.sms.core.ws.mts.listener.smsnotification.DeliveryInformation;
import com.klikmee.sms.core.ws.mts.listener.smsnotification.DeliveryStatus;
import com.klikmee.sms.core.ws.mts.listener.smsnotification.SmsMessage;
import com.klikmee.sms.core.ws.mts.listener.subscription.SubscriptionDetail;

public class MTSCommunicatorImpl implements Communicator, MTSCommunicator {

	private Core				core							= null;

	// wsdl urls
	private URL					sendSMSURL						= null;
	private URL					receiveMessageURL				= null;
	// send SMS service proxy
	private SendSms				sendsmsProxy					= null;
	private ReceiveMessage		receiveMessageProxy				= null;
	private String				deliveryReportNotificationURL	= null;
	private String				sendSmsOverrideAddress			= null;
	// ref to mts provider
	private Provider			mtsProvider;
	private SubscriptionManager	subscriptionManager				= null;
	private ProviderDAO			providerDAO						= null;

	private String				providerId						= null;

	private List<SendSmsIntercepter> sendSmsIntercepters = null;
	// logger
	private Logger				logger							= Logger.getLogger( MTSCommunicatorImpl.class );
	
	// init method
	public void init() {

		// init ws proxies
		this.sendsmsProxy = new SendSmsService( sendSMSURL ).getSendSms();
		this.receiveMessageProxy = new ReceiveMessageService( this.receiveMessageURL ).getReceiveMessage();

		// override endpoing address
		if (this.sendSmsOverrideAddress != null) {
			BindingProvider sendsmsBP = (BindingProvider) this.sendsmsProxy;
			Map<String, Object> sendsmsRequestContext = sendsmsBP.getRequestContext();
			sendsmsRequestContext.put( BindingProvider.ENDPOINT_ADDRESS_PROPERTY, this.sendSmsOverrideAddress );

			logger.info( String.format( "Sendsms service url [%s]", this.sendSmsOverrideAddress ) );
		}
		// load mts provider
		this.mtsProvider = this.providerDAO.findByName( Constants.PROVIDER_MTS );

		logger.info( "MTS communicator successfuly initialized." );
	}

	/**
	 * utility method to set soap header authentication. MTS uses username token security standard by OASIS.
	 * 
	 * @param username
	 */
	private void setHeaders( String username ) {
		Client client = ClientProxy.getClient( sendsmsProxy );
		Endpoint cxfEndpoint = client.getEndpoint();

		Map<String, Object> outProps = new HashMap<String, Object>();
		outProps.put( WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN );
		outProps.put( WSHandlerConstants.USER, username );
		outProps.put( WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_DIGEST );

		outProps.put( WSHandlerConstants.PW_CALLBACK_CLASS, ClientPasswordCallback.class.getName() );
		// put additional stuff in header
		outProps.put( WSHandlerConstants.ADD_UT_ELEMENTS, WSConstants.NONCE_LN + " " + WSConstants.CREATED_LN );

		WSS4JOutInterceptor interceptor = new WSS4JOutInterceptor( outProps );
		cxfEndpoint.getOutInterceptors().add( interceptor );

	}

	public void sendMessage( OutgoingMessage outgoingMessage ) {

		// set request parameters

		// set recipient of message
		List<String> addresses = new LinkedList<String>();
		// check if outgoing message has session id
		if (outgoingMessage.getMtsSessionId() != null) {
			addresses.add( outgoingMessage.getMtsSessionId() );
		}
		// otherwise use phone number
		else {
			addresses.add( "tel:" + outgoingMessage.getToNumber() );
		}

		// sender name
		//		String senderName = outgoingMessage.getPaymentInvoiceList().iterator().next().getServiceChargingCategory().getService().getMtsServiceName();
		String senderName = outgoingMessage.getFromNumber();

		// charging info
		ChargingInformation charging = new ChargingInformation();
		charging.setCode( "" );
		charging.setDescription( "" );
		charging.setAmount( null );
		charging.setCurrency( "" );
		// TODO: check are the other field in this object necessary for regular sms

		// message text
		String message = outgoingMessage.getMessage();

		// status report delivery info
		SimpleReference receiptRequest = new SimpleReference();
		// correlator used to link delivery report with this message. Core message id is used for this
		receiptRequest.setCorrelator( String.valueOf( outgoingMessage.getId() ) );
		receiptRequest.setEndpoint( this.deliveryReportNotificationURL );
		// this parameter is ignored
		receiptRequest.setInterfaceName( "not used" );

		// set headers
		// username is composed of <serviceid>@<providerid>
		String username = outgoingMessage.getPaymentInvoiceList().iterator().next().getServiceChargingCategory().getMtsSendingId() + "@" + this.providerId;
		this.setHeaders( username );

		try {

			String requestIdentifier = sendsmsProxy.sendSms( addresses, senderName, null, message, receiptRequest );
			outgoingMessage.setProvidersMesssageId( requestIdentifier );
			// if everything is ok, notify core 
			core.onMessageSent( outgoingMessage );

		} catch (PolicyException_Exception e) {
			// throw exception back to the core
			//			throw new RuntimeException( e );

			logger.fatal( String.format( "Policy exception while trying to send message id = %s", outgoingMessage.getId() ) );
		} catch (ServiceException_Exception e) {
			// throw exception back to the core
			//			throw new RuntimeException( e );
			logger.fatal( String.format( "Service exception while trying to send message id = %s", outgoingMessage.getId() ) );
		}
	}

	public void onDeliveryReceipt( String correlator, DeliveryInformation deliveryInformaion ) {
		// this object will be forwarded to core
		OutgoingMessage outgoingMessage = new OutgoingMessage();

		// set provides messages id 
		outgoingMessage.setId( Long.parseLong( correlator ) );
		// resolve status report
		int resolvedStatus = resolveDeliveryStatus( deliveryInformaion.getDeliveryStatus() );
		outgoingMessage.setDeliveryStatus( resolvedStatus );

		outgoingMessage.setProvider( this.mtsProvider );

		// try to deliver message to core
		try {
			core.enqueueStatusReport( outgoingMessage );
			logger.debug( String.format( "Successfuly forwarded delivery status to core, corellator = [%s]", correlator ) );
		} catch (Exception e) {
			logger.fatal( String.format( "Failed to forward delivery status to core, correlator = [%s] ", correlator ) );
		}
	}

	/**
	 * This method is invoked when new sms message is received. Message parameters are extracted and Incoming message is created and forwarded to core.
	 */
	public void onSmsReception( String correlator, SmsMessage mtsMessage ) {
		// this object will be forwarded to core
		IncomingMessage incomingMessage = new IncomingMessage();
		incomingMessage.setTypeOfMessage( Constants.MESSAGE_TYPE_SMS );
		incomingMessage.setOurIncomingTime( new Date() );
		incomingMessage.setProvider( this.mtsProvider );

		incomingMessage.setProviderMessageId( correlator );
		incomingMessage.setToNumber( mtsMessage.getSmsServiceActivationNumber().substring( "tel:".length() ) );

		// extract phone number and session id

		String[] sessionParameters = this.extractPhonenumberFromSessionId( mtsMessage.getSenderAddress() );
		if (sessionParameters != null) {
			incomingMessage.setMtsSessionId( sessionParameters[0] );
			incomingMessage.setFromNumber( sessionParameters[1] );
		} else {
			// use format tel:38164100100
			incomingMessage.setFromNumber( mtsMessage.getSenderAddress().substring( "tel:".length() ) );
		}

		incomingMessage.setMessage( mtsMessage.getMessage() );

		// try to delive message to core
		try {
			core.enqueueIncomingMessage( incomingMessage );
			// log debug message
			logger.debug( String.format( "Message with reference id = [%s] forwarded to core", correlator ) );

		} catch (Exception e) {
			logger.fatal( String.format( "Failed to deliver message to core, reference id = [%s]", correlator ), e );
		}

	}

	public String[] extractPhonenumberFromSessionId( String sessionId ) {
		// first element of array is sessionId and the second is sender's phone number
		String[] incommingParameters = new String[2];

		if (sessionId == null) {
			return null;
		}

		int indexOfSeparator = sessionId.indexOf( '#' );

		// check if sessionId contains #
		if (indexOfSeparator != -1) {
			// extract values
			incommingParameters[0] = sessionId.substring( 0, indexOfSeparator );
			incommingParameters[1] = sessionId.substring( indexOfSeparator + 1 );
		}

		return incommingParameters;
	}

	public void sendBulkMessage( BulkMessage bulkMessage ) {
		List<String> addresses = new LinkedList<String>();
		// add necessary prefixes 
		// check if recipient is single user
		String recipient = "";
		if (bulkMessage.getToNumber().startsWith( "381" )) {
			recipient = "tel:" + bulkMessage.getToNumber();
		}
		// if not then it's a name of download list
		else {
			recipient = "dl:" + bulkMessage.getToNumber();
		}
		addresses.add( recipient );

		// set sender name
		String senderName = bulkMessage.getSenderId();

		// charging should stay empty
		ChargingInformation charging = new ChargingInformation();

		// message
		String message = bulkMessage.getText();

		// this sould be null because it's not possible to get delivery for bulk
		SimpleReference receiptRequest = null;
		try {
			String username = bulkMessage.getChargingCategory().getMtsSendingId() + "@" + this.providerId;
			setHeaders( username );
			String sendBulkResult = sendsmsProxy.sendSms( addresses, senderName, charging, message, receiptRequest );
			core.onBulkMessageSent( bulkMessage );

		} catch (PolicyException_Exception e) {
			logger.fatal( String.format( "Policy exception while trying to send bulk message id = %s", bulkMessage.getId() ) );
		} catch (ServiceException_Exception e) {
			logger.fatal( String.format( "Service exception while trying to send bulk message id = %s", bulkMessage.getId() ) );
		}

	}

	public SubscriptionDetail getSubscriptionDetail( String serviceId, String message ) throws com.klikmee.sms.core.ws.mts.listener.subscription.ServiceException_Exception {

		// try to find distribution list data
		try {

			Hashtable<String, String> paramsTable = subscriptionManager.getSubscriptionDetail( serviceId, message );

			SubscriptionDetail subscriptionDetail = new SubscriptionDetail();
			subscriptionDetail.setDistributionList( paramsTable.get( SubscriptionManager.DISTRIBUTION_LIST_ID ) );
			subscriptionDetail.setExtensionParameter( paramsTable.get( SubscriptionManager.DISTRIBUTION_LIST_NAME ) );

			return subscriptionDetail;

		} catch (NoDistributionListFoundException e) {
			// it this exception is cought then no distribution list is found for query, throw service ex
			throw new com.klikmee.sms.core.ws.mts.listener.subscription.ServiceException_Exception();
		}

		// in case of any other exception
		catch (Exception e) {
			throw new com.klikmee.sms.core.ws.mts.listener.subscription.ServiceException_Exception( "Unexpected error", e );
		}

	}

	public void receiveSubscription( String msisdn, String serviceId, String distributionList, Date validTo ) {
		// forward request to subaciption mngr 
		this.subscriptionManager.receiveSubacription( msisdn, serviceId, distributionList, validTo );

	}

	public void receiveUnsubscription( String msisdn, String serviceId, String distributionListId ) {
		// forward request to subscription mngr
		this.subscriptionManager.receiveUnsubscription( msisdn, serviceId, distributionListId );

	}

	public void triggerService( String serviceId, String distributionList ) {
		// TODO Auto-generated method stub

	}

	public void receiveSubscriptionExtension( String msisdn, String serviceId, String distributionList, Date validTo ) {
		// forward request to subscription mngr
		this.subscriptionManager.receiveSubscriptionExtension( msisdn, serviceId, distributionList, validTo );
	}

	/**
	 * Invoked when MMS message arrives from MTS. if message identifier parameter is present, it means that there is an actual message waiting on MTS gateway, so we pick it up with a call to
	 * getMessage(identifier) method of receiveMessage Service. If not, then it's a MMS message with only plain text and that text is contained in message parameter.
	 * 
	 */

	public void notifyMessageReception( String correlator, String messageIdentifier, String message, Date time, String messageServiceActivationNumber, String senderAddress, String subject,
			MessagePriority priority ) {

		// set up incoming msg

		IncomingMessage incomingMessage = new IncomingMessage();
		incomingMessage.setProvider( mtsProvider );

		// check if message contains messageIdentifier
		if (messageIdentifier == null || messageIdentifier.isEmpty()) {
			// get message attachments

		}

	}

	/**
	 * This method gets attachments for MMS message using getMessage web service proxy.
	 * 
	 * @param messageIdentifier
	 *            id of the mms message which holds the attachments
	 * @return attachment as byte array
	 * @throws com.klikmee.sms.core.ws.mts.client.receivemsg.ServiceException_Exception
	 * @throws com.klikmee.sms.core.ws.mts.client.receivemsg.PolicyException_Exception
	 * @throws IOException
	 */
	public byte[] getMMSAttachments( String messageIdentifier ) throws com.klikmee.sms.core.ws.mts.client.receivemsg.PolicyException_Exception,
			com.klikmee.sms.core.ws.mts.client.receivemsg.ServiceException_Exception, IOException {
		// call getMessage method on proxy
		receiveMessageProxy.getMessage( messageIdentifier );

		// Although method is void attachments are returned
		Map<String, Object> responseContext = ((BindingProvider) this.receiveMessageProxy).getResponseContext();
		HashMap attatchments = (HashMap) responseContext.get( MessageContext.INBOUND_MESSAGE_ATTACHMENTS );
		// get the datahandler key
		String key = attatchments.keySet().toString();
		// remove first and last char
		key = key.substring( 1, key.length() - 1 );
		DataHandler dataHandler = (DataHandler) attatchments.get( key );

		// read the datahandler input stram
		InputStream inputStream = dataHandler.getInputStream();
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		// read until end of stram is reached
		int next = inputStream.read();
		while (next != -1) {
			byteArrayOutputStream.write( next );
			next = inputStream.read();
		}

		byteArrayOutputStream.flush();
		return byteArrayOutputStream.toByteArray();

	}

	public void notifyMessageDeliveryReceipt( String correlator, String address, com.klikmee.sms.core.ws.mts.listener.msgnotification.DeliveryStatus deliveryStatus ) {
		// TODO Auto-generated method stub

	}
	
	@Override
	public List<SendSmsIntercepter> getSendSmsIntercepters() {
		// TODO Auto-generated method stub
		return null;
	}

	private int resolveDeliveryStatus( DeliveryStatus mtsStatus ) {
		switch (mtsStatus) {
			case DELIVERED_TO_NETWORK:
			case DELIVERED_TO_TERMINAL:
				return DeliveryReports.DELIVERED;

			case DELIVERY_IMPOSSIBLE:
				return DeliveryReports.Mts.DELIVERY_IMPOSSIBLE;
			case DELIVERY_NOTIFICATION_NOT_SUPPORTED:
				return DeliveryReports.Mts.DELIVERY_NOTIFICATION_NOT_SUPPORTED;

			case DELIVERY_UNCERTAIN:
				return DeliveryReports.Mts.DELIVERY_UNCERTAIN;
			case MESSAGE_WAITING:
				return DeliveryReports.Mts.DELIVERY_UNCERTAIN;
			default:
				return -1;
		}
	}

	public void setSendSMSURL( URL sendSMSURL ) {
		this.sendSMSURL = sendSMSURL;
	}

	public void setReceiveMessageURL( URL receiveMessageURL ) {
		this.receiveMessageURL = receiveMessageURL;
	}

	public void setSubscriptionManager( SubscriptionManager subscriptionManager ) {
		this.subscriptionManager = subscriptionManager;
	}

	public void setCore( Core core ) {
		this.core = core;
	}

	public void setProviderDAO( ProviderDAO providerDAO ) {
		this.providerDAO = providerDAO;
	}

	public void setSendSmsOverrideAddress( String sendSmsOverrideAddress ) {
		this.sendSmsOverrideAddress = sendSmsOverrideAddress;
	}

	public String getProviderId() {
		return providerId;
	}

	public void setProviderId( String providerId ) {
		this.providerId = providerId;
	}

	public URL getSendSMSURL() {
		return sendSMSURL;
	}

	public String getDeliveryReportNotificationURL() {
		return deliveryReportNotificationURL;
	}

	public void setDeliveryReportNotificationURL( String deliveryReportNotificationURL ) {
		this.deliveryReportNotificationURL = deliveryReportNotificationURL;
	}

	public String getSendSmsOverrideAddress() {
		return sendSmsOverrideAddress;
	}

	public void setSendSmsIntercepters(List<SendSmsIntercepter> sendSmsIntercepters) {
		this.sendSmsIntercepters = sendSmsIntercepters;
	}
	
	/**
	 * 
	 * Inner class used to provide callback for MTS web service auth.
	 * 
	 */

	public static class ClientPasswordCallback implements CallbackHandler {

		private static Logger		logger			= Logger.getLogger( ClientPasswordCallback.class );

		private static String		password		= null;
		private static Properties	CORE_SETTINGS	= new Properties();

		static {

			InputStream propertiesStream = MTSCommunicatorImpl.class.getClassLoader().getResourceAsStream( "sms-core.properties" );
			try {
				CORE_SETTINGS.load( propertiesStream );
				// read password from properties file
				password = CORE_SETTINGS.getProperty( "MTS_PROVIDER_PASSWORD" );
			} catch (IOException e) {
				logger.fatal( "Could not read sms-core.properties" );
			}
		}

		public void handle( Callback[] callbacks ) throws IOException, UnsupportedCallbackException {

			// if password is null, break sms sending

			if (password == null) {
				throw new RuntimeException( "MTS password not set ... " );
			}
			WSPasswordCallback callback = (WSPasswordCallback) callbacks[0];
			callback.setPassword( password );

		}

	}



}
